/*
 * TMap Designer - A Thematic Map design software
 * Copyright (C) 2006 World Health Organization
 * http://www.who.int
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package who.tmap.imp.shapefile.impl;

import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.EndianUtils;

import who.tmap.bo.AttrDef;
import who.tmap.bo.Attrs;
import who.tmap.bo.BaseMap;
import who.tmap.common.TMapException;
import who.tmap.exp.DBaseExportAdapter;
import who.tmap.toolkit.impl.AttrHelper;
import who.tmap.toolkit.shape.MapShape;
import who.tmap.toolkit.shape.PolygonShape;

public class ShapeFileExport extends BaseShapeFileIO {

	private static final int VERSION =1000;
	private static final int POLYGON_TYPE =5;
	
    private FileOutputStream fos;
    private DataOutputStream dos; 	
	
	public ShapeFileExport() {
		super();
	}
	
    /**
     * Open the shape file
     * @param file
     * @throws FileNotFoundException
     */
    public void openFile( File file ) throws FileNotFoundException {
        this.file =file;
        
        fos = new FileOutputStream( file );
        dos = new DataOutputStream( fos );
    }
    
    /**
     * Close the shape file
     * @throws IOException
     */
    public void closeFile() throws IOException {
        dos.close();
        fos.close();
    }    	
	
    public void writeShapeFile( BaseMap baseMap, Attrs attrs, File file ) throws IOException, TMapException {
        Exception e2 =null;        
        try {
	        openFile( file );
	        
	        List<Polygon> polygons =computeRecords( baseMap.getShapes() );
	        
	        int fileLength =computeFileLength( polygons );
	        writeMainFileHeader( baseMap, fileLength );
	        
	        writeRecords( polygons );
	        
	        writeDataFile( polygons, attrs, file );
	        
        } catch( Exception e ) {
            e2 =e;
        } finally {
            closeFile();
        }
        
        if ( e2 != null ) {
            throw new TMapException( e2 );
        }
        
    }
    
    /**
     * Write dbf file containing map attributes
     * @param polygons
     * @param attrs
     * @param file
     */
    @SuppressWarnings("unchecked")
	private void writeDataFile( List<Polygon> polygons, Attrs attrs, File file ) throws TMapException {
    	Map<String, List<Object>> tmp =new HashMap<String, List<Object>>();
    	
    	for( List<Object> row : attrs.getRows() ) {
    		Object joindId =row.get( attrs.getJoinIdIdx() );
    		String joinIdStr =joindId != null ? joindId.toString() : "";
    		
    		tmp.put( joinIdStr.toUpperCase(), row );
    	}
    	
    	// Rearrange order to match polygons in the shape file
		AttrHelper attrHelper =new AttrHelper();    	
    	List<List> orderedRows =new ArrayList<List>();
    	for( Polygon polygon : polygons ) {
    		List<Object> row =tmp.get( polygon.getName().toUpperCase() );
    		
    		// No data for this polygon, 
    		// so we create a row with the join id and empty data
    		if ( row == null ) {

    			row =new ArrayList<Object>();
    			
    			for( int ct =0; ct < attrs.getHeader().size(); ct++ ) {
    				if ( ct == attrs.getJoinIdIdx() ) {
    					row.add( polygon.getName() );
    				} else {
    					AttrDef def =attrs.getHeader().get( ct );
    					Object val =attrHelper.getNewInstanceFromClassName( def.getType() );
    					row.add( val );
    				}
    			}
    		}
    		
    		orderedRows.add( row );
    	}
    	
    	List<List> rowsSav =attrs.getRows();
    	attrs.setRows( orderedRows );
    	
    	Exception e2 =null;
    	try {
    		new DBaseExportAdapter().export( DBaseExportAdapter.convertShapeFileNameToDBaseFileName( file ), 
    										 attrs );
    	} catch( Exception e ) {
    		e2 =e;
    	} finally {
    		attrs.setRows( rowsSav );
    	}
    	
    	if ( e2 != null ) {
    		throw new TMapException( e2 );
    	}
    }
    
    private int computeFileLength( List<Polygon> polygons ) {
    	int length =96;
    	
    	for( Polygon polygon : polygons ) {
    		length+=computeFileLength( polygon ) + 4;
    	}
    	
    	return length;
    }
    
    private int computeFileLength( Polygon polygon ) {
    	return 24 + ( polygon.getTotalNbPoints() * 8 );
    }
    
    private void writeMainFileHeader( BaseMap baseMap, int fileLength ) throws Exception {
    	// Big endian !
    	dos.writeInt( SHAPE_FILE_CODE );

        // 5 unused integers here
    	writeInt( 0 );
    	writeInt( 0 );
    	writeInt( 0 );
    	writeInt( 0 );
    	writeInt( 0 );    	
        
    	// Big endian !
    	dos.writeInt( fileLength );
        writeInt( VERSION ); 
        writeInt( POLYGON_TYPE ); 
        
        writeDouble( baseMap.getMinX() );
        writeDouble( baseMap.getMinY() );
        writeDouble( baseMap.getMaxX() );
        writeDouble( baseMap.getMaxY() );
        writeDouble( 0d ); // zmin
        writeDouble( 0d ); // zmax
        writeDouble( 0d ); // mmin
        writeDouble( 0d ); // mmax
    }
    
    private List<Polygon> computeRecords( List<MapShape> shapes ) throws Exception {
    	List<Polygon> polygons =new ArrayList<Polygon>();
		
    	// Group polygons by name
    	Map<String, List<PolygonShape>> polByName =new HashMap<String, List<PolygonShape>>();
    	for( MapShape shape : shapes ) {
    		if ( shape instanceof PolygonShape ) {
    			PolygonShape polShape =(PolygonShape)shape;
    			
    			List<PolygonShape> pols =polByName.get( polShape.getName().toUpperCase() );
    			if ( pols == null ) {
    				pols =new ArrayList<PolygonShape>();
    				polByName.put( polShape.getName().toUpperCase(), pols );
    			} 
    			
    			pols.add( polShape );
    			
    		}
    	}
    	
    	for( List<PolygonShape> polList : polByName.values() ) {
    		
			Polygon polygon =new Polygon();
			polygons.add( polygon );
			int totalNbPoints =0; 
    		
    		for( PolygonShape polShape : polList ) {
    				
    			polygon.setName( polShape.getName() );

    			PolygonPart part =new PolygonPart();
    			polygon.addPart( part );
    			
    			int type =-1;
    			float[] c =new float[6];
    			float x =Float.MIN_VALUE, y =Float.MIN_VALUE;
    			float lastMoveToX =Float.MIN_VALUE, lastMoveToY =Float.MIN_VALUE;    			
    			
    			GeneralPath gp =polShape.getGeneralPath();
    			PathIterator it =gp.getPathIterator(null);
    			while( !it.isDone() ) {
    				type = it.currentSegment(c);
    				
    				switch (type) {
    					case PathIterator.SEG_MOVETO:
    						x =c[0];
    						y =c[1];
    						lastMoveToX =x;
    						lastMoveToY =y;
    						break;
    						
    					case PathIterator.SEG_LINETO:
    						x =c[0];
    						y =c[1];
    						break;
    						
    					case PathIterator.SEG_CLOSE:
    						x =lastMoveToX;
    						y =lastMoveToY;
    						break;
    				} 
    				
    				part.addPoint( new Point2D.Float( x, -y ) );
    				
    				it.next();
    				
    				// There is another part
    				if ( type == PathIterator.SEG_CLOSE && !it.isDone() ) {
    					part =new PolygonPart();
    					polygon.addPart( part );
    				}
    				
    				totalNbPoints++;
    			}
    			
    			// In case the last part is not closed
				if ( type != PathIterator.SEG_CLOSE && type != -1 ) {
					part.addPoint( new Point2D.Float( x, -y ) );
					totalNbPoints++;
				}
    			
				Rectangle2D bounds =polygon.getBounds();
				if ( bounds != null ) {
					bounds.add( polShape.getBounds() );
				} else {
					bounds =(Rectangle2D)polShape.getBounds().clone();
				}
				polygon.setBounds( bounds );
				polygon.setTotalNbPoints( totalNbPoints );
  			    			
    		}
    	}
    	
    	return polygons;
    }    
    
/*    private List<Polygon> computeRecords( List<MapShape> shapes ) throws Exception {
    	List<Polygon> polygons =new ArrayList<Polygon>();
		
    	for( MapShape shape : shapes ) {
    		if ( shape instanceof PolygonShape ) {
    			PolygonShape polShape =(PolygonShape)shape;
    			
    			Polygon polygon =new Polygon();
    			polygons.add( polygon );
    			
    			polygon.setName( polShape.getName() );
    			int totalNbPoints =0;    			

    			PolygonPart part =new PolygonPart();
    			polygon.addPart( part );
    			
    			int type =-1;
    			float[] c =new float[6];
    			float x =Float.MIN_VALUE, y =Float.MIN_VALUE;
    			float lastMoveToX =Float.MIN_VALUE, lastMoveToY =Float.MIN_VALUE;    			
    			
    			GeneralPath gp =polShape.getGeneralPath();
    			PathIterator it =gp.getPathIterator(null);
    			while( !it.isDone() ) {
    				type = it.currentSegment(c);
    				
    				switch (type) {
    					case PathIterator.SEG_MOVETO:
    						x =c[0];
    						y =c[1];
    						lastMoveToX =x;
    						lastMoveToY =y;
    						break;
    						
    					case PathIterator.SEG_LINETO:
    						x =c[0];
    						y =c[1];
    						break;
    						
    					case PathIterator.SEG_CLOSE:
    						x =lastMoveToX;
    						y =lastMoveToY;
    						break;
    				} 
    				
    				part.addPoint( new Point2D.Float( x, -y ) );
    				
    				it.next();
    				
    				// There is another part
    				if ( type == PathIterator.SEG_CLOSE && !it.isDone() ) {
    					part =new PolygonPart();
    					polygon.addPart( part );
    				}
    				
    				totalNbPoints++;
    			}
    			
    			// In case the last part is not closed
				if ( type != PathIterator.SEG_CLOSE && type != -1 ) {
					part.addPoint( new Point2D.Float( x, -y ) );
					totalNbPoints++;
				}
    			
				polygon.setBounds( polShape.getBounds() );
				polygon.setTotalNbPoints( totalNbPoints );
  			    			
    		}
    	}
    	
    	return polygons;
    }*/
    
    private void writeRecords( List<Polygon> polygons ) throws Exception {
    	int recordNumber =0;
    	
    	for( Polygon polygon : polygons ) {
    		dos.writeInt( ++recordNumber );
    		dos.writeInt( computeFileLength( polygon ) );
    		writeInt( POLYGON_TYPE ); 
    		
    		// Box
    		writeDouble( polygon.getBounds().getMinX() );
    		writeDouble( -polygon.getBounds().getMaxY() );
    		writeDouble( polygon.getBounds().getMaxX() );
    		writeDouble( -polygon.getBounds().getMinY() );
    		
    		writeInt( polygon.getParts().size() );
    		writeInt( polygon.getTotalNbPoints() );
    		
    		int partIdx =0;
    		for( PolygonPart part : polygon.getParts() ) {
    			writeInt( partIdx );
    			partIdx+=part.getPoints().size();
    		}
    		
    		for( PolygonPart part : polygon.getParts() ) {
    			for( Point2D pt : part.getPoints() ) {
    				writeDouble( pt.getX() );
    				writeDouble( pt.getY() );
    			}
    		}
    	}
    }
    
    /**
     * Writes a little endian int
     * @return little endian int
     * @throws IOException
     */
    private void writeInt( int val ) throws IOException
    {
    	EndianUtils.writeSwappedInteger( dos, val );
    }
    
    /**
     * Writes a little endian double
     * @return little endian double
     * @throws IOException
     */
    private void writeDouble( double val ) throws IOException
    {
    	EndianUtils.writeSwappedDouble( dos, val );
    }        
    
    private class Polygon {
    	private Rectangle2D bounds;
    	private List<PolygonPart> parts;
    	private int totalNbPoints;
    	private String name;
    	
    	public Polygon() {
    		parts =new ArrayList<PolygonPart>();
    		totalNbPoints =0;
    	}
    	
    	public void addPart( PolygonPart part ) {
    		parts.add( part );
    	}
    	
    	public List<PolygonPart> getParts() {
    		return parts;
    	}

		public Rectangle2D getBounds() {
			return bounds;
		}

		public void setBounds(Rectangle2D bounds) {
			this.bounds = bounds;
		}
		
		public void setTotalNbPoints( int totalNbPoints ) {
			this.totalNbPoints =totalNbPoints;
		}

		public int getTotalNbPoints() {
			return totalNbPoints;
		}

		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}		
    	    	
    }
    
    private class PolygonPart {
    	private List<Point2D> points;
    	
    	public PolygonPart() {
    		points =new ArrayList<Point2D>();
    	}
    	
    	public void addPoint( Point2D pt ) {
    		points.add( pt );
    	}
    	
    	public List<Point2D> getPoints() {
    		return points;
    	}
    }
    
}
